Práctica 1: Introducción a la programación de sistemas en Linux
Familiarizarse con el entorno de desarrollo de aplicaciones C en GNU/Linux, y comprender los conceptos de proyecto y ejecutable en este contexto.
Familiarizarse con el manejo básico del shell y aprender a desarrollar shell scripts sencillos.
Para poder realizar con éxito la práctica el alumno debe haber leído y comprendido los siguientes documentos facilitados por el profesor:
Presentación “Introducción al entorno de desarrollo”, que nos introduce al entorno GNU/Linux que utilizaremos en el laboratorio, y describe cómo trabajar con proyectos C con Makefile.
Presentación “Revisión: Programación en C”, que realiza un repaso de los conocimientos de C necesarios para realizar con éxito las prácticas, haciendo especial hincapié en los errores que cometen habitualmente los estudiantes menos experimentados en el lenguaje C.
Manual del laboratorio titulado “Entorno de desarrollo C para GNU/Linux”, que describe las herramientas que componen el entorno de desarrollo que vamos a utilizar, así como las funciones básicas de la biblioteca estándar de C que los alumnos deben conocer.
Presentación “Introducción a Bash”, que presenta una breve introducción al interprete de órdenes (shell) Bash.
Analizar el código del programa show_file.c, que lee byte a byte el contenido de un fichero, cuyo nombre se pasa como parámetro, y lo muestra por pantalla usando funciones de la biblioteca estándar de “C”. Responda a las siguientes preguntas:
show_file.c
show_file
gcc
make
Realice las siguientes modificaciones en el programa show_file.c:
fread()
getc()
putc()
fwrite()
#include <stdio.h> #include <stdlib.h> int main(int argc, char* argv[]) { FILE* file=NULL; int c,ret; if (argc!=2) { fprintf(stderr,"Usage: %s <file_name>\n",argv[0]); exit(1); } /* Open file */ if ((file = fopen(argv[1], "r")) == NULL) err(2,"The input file %s could not be opened",argv[1]); /* Read file byte by byte */ while ((c = getc(file)) != EOF) { /* Print byte to stdout */ ret=putc((unsigned char) c, stdout); if (ret==EOF){ fclose(file); err(3,"putc() failed!!"); } } fclose(file); return 0; }
El programa badsort-ptr.c, cuyo código fuente se muestra a continuación, ha sido desarrollado para realizar una ordenación por el método de la burbuja aplicada a un array de pares (cadena de caracteres, entero) inicializado dentro del programa. El programa emplea aritmética de punteros para acceder a los distintos elementos del array durante el recorrido. Lamentablemente, el programador ha cometido algunos errores. Utilizando un depurador de C (p.ej.,gdb) el alumno debe encontrar y corregir los errores.
gdb
#include <stdio.h> typedef struct { char data[4096]; int key; } item; item array[] = { {"bill", 3}, {"neil", 4}, {"john", 2}, {"rick", 5}, {"alex", 1}, }; void sort(item *a, int n) { int i = 0, j = 0; int s = 1; item* p; for(; i < n & s != 0; i++) { s = 0; p = a; j = n-1; do { if( p->key > (p+1)->key) { item t = *p; *p = *(p+1); *(p+1) = t; s++; } } while ( --j >= 0 ); } } int main() { int i; sort(array,5); for(i = 0; i < 5; i++) printf("array[%d] = {%s, %d}\n", i, array[i].data, array[i].key); return 0; }
Estudiar el código y el funcionamiento del programa show-passwd.c, que lee el contenido del fichero del sistema /etc/passwd e imprime por pantalla (o en otro fichero dado) las distintas entradas de/etc/passwd –una por línea–, así como los distintos campos de cada entrada. El fichero /etc/passwd almacena en formato de texto plano información esencial de los usuarios del sistema, como su identificador numérico de usuario o grupo así como el programa configurado como intérprete de órdenes (shell) predeterminado para cada usuario. Para obtener más información sobre este fichero se ha de consultar su página de manual: man 5 passwd
show-passwd.c
/etc/passwd
man 5 passwd
El modo de uso del programa puede consultarse invocándolo con la opción -h:
$ ./show-passwd -h Usage: ./show-passwd [ -h | -v | -p | -o <output_file> ]
Las opciones -v y -p, permiten configurar el formato en el que el programa imprime la información de /etc/passwd. Las citadas opciones activan respectivamente el modo verbose(por defecto) o pipe. La opción -o, que acepta un argumento obligatorio, permite selecionar un fichero para la salida del programa alternativo a la salida estándar.
-v
-p
verbose
pipe
-o
Uno de los principales objetivos de este ejercicio es que el estudiante se familiarize con tres funciones muy útiles empleadas por el programa show-passwd.c , y cuya página de manual debe consultarse:
int sscanf(const char *s, const char *format, ...);
Variante de scanf() que permite leer con formato a partir de un buffer de caracteres pasado como primer parámetro (s) . La función almacena en variables del programa, pasadas como argumento tras la cadena de formato, el resultado de convertir los distintos “tokens” de s de ASCII a binario.
scanf()
s
char *strsep(char **stringp, const char *delim);
Permite dividir una cadena de caracteres en tokens, proporcionando como segundo parámetro la cadena delimitadora de esos tokens. Como se puede observar en el programa show-passwd.c, esta función se utiliza para extraer los distintos campos almacenados en cada línea del fichero /etc/passwd, que están separados por ":". La función strsep() se usa típicamente en un bucle, que para tan pronto como el token devuelto es NULL. El primer argumento de la función es un puntero por referencia. Antes de comenzar el bucle, *stringp debe apuntar al comienzo de la cadena que deseamos procesar. Cuando strsep() retorna, *stringp apunta al resto de la cadena que queda por procesar.
":"
strsep()
*stringp
int getopt(int argc, char *const argv[], const char *optstring);
Esta función es la más sofisticada de las tres, y permite procesar cómodamente las distintas opciones de la línea de comando que acepta un programa C. La función suele invocarse desde main(), y sus dos primeros parámetros coinciden con los argumentos argc y argv pasados a main(). El parámetro optstring sirve para indicar de forma compacta a getopt() cuáles son las opciones que el programa acepta –cada una identificada por una letra–, y si éstas a su vez aceptan parámetros obligatorios u opcionales.
main()
argc
argv
optstring
getopt()
El estudiante deberá familiarizarse con esta función mediante el estudio del código fuente del programa, y la consulta de la página de manual de getopt(): man 3 getopt
man 3 getopt
Deben tenerse en cuenta las siguientes consideraciones:
char* optarg
optarg
NULL
int optind
Responda a las siguientes preguntas:
passwd_entry_t
defs.h
char*
parse_passwd()
clone_string()
strcpy()
campo=cadena_existente;
line
Realice las siguientes modificaciones en el programa show-passwd.c:
Añada la opción -i <inputfile> para especificar una ruta alternativa para el fichero passwd. Hacer una copia de /etc/passwd en otra ubicación para verificar el correcto funcionamiento de esta nueva opción.
-i <inputfile>
passwd
Implemente una nueva opción -c en el programa, que permita mostrar los campos en cada entrada de passwd como valores separados por comas (CSV) en lugar de por ":".
-c
Desarrollar un programa student-record que permita crear ficheros binarios que almacenen un conjunto de registros con información de distintos estudiantes, y también permita consultar/imprimir información almacenada en estos ficheros. Cada estudiante estará representado mediante 4 campos: identificador numérico único, NIF, nombre, y apellidos. El fichero binario ha de contener una cabecera (entero de 32 bits) que indique cuál es el número de registros almacenados, y a continuación incluir los registros de estudiantes en formato binario, uno detrás del otro.
student-record
Cada registro de estudiantes estará representado en memoria mediante la siguiente estructura:
#define MAX_CHARS_NIF 9 typedef struct { int student_id; char NIF[MAX_CHARS_NIF+1]; char* first_name; char* last_name; } student_t;
Por cada registro debe escribirse en el fichero (representación en disco) el identificador numérico único (4 bytes), seguido de las cadenas de caracteres asociadas a los tres campos restantes. Almacenar cada una de las cadenas en el fichero conlleva escribir todos sus caracteres, incluyendo el terminador (\0). Esto es esencial para permitir posteriormente la lectura correcta de los campos del fichero.
\0
El modo de uso del programa debe poder consultarse con la opción -h, del siguiente modo:
$ ./student-records -h Usage: ./student-records -f file [ -h | -l | -c | -a | -q [ -i|-n ID] ] ] [ list of records ]
Además de -h, el programa implementará opciones para crear (-c) y listar (-l) ficheros de registros de estudiantes, así como para poder añadir nuevos registros al final de un fichero existente -a, o realizar búsquedas (queries) de registros específicos (-q) por identificador de estudiante (-i) o NIF -n. En el caso de las opciones -c y -a, será preciso indicar en la línea de comando una lista de registros de estudiantes a almacenar en el fichero. Cada registro de la lista especificará los campos de cada estudiante mediante una cadena de caracteres con elementos separados por ":". Por ejemplo, considérese el siguiente comando para crear un nuevo fichero de estudiantes llamado database que almacenará 2 registros:
-h
-l
-a
-q
-i
-n
database
$ ./student-records -f database -c 27:67659034X:Chris:Rock 34:78675903J:Antonio:Banderas 2 records written succesfully
El programa deberá construirse de cero, pero se recomienda reutilizar código y algunas ideas de diseño del proyecto del ejercicio anterior. Se aconseja también implementar las siguientes funciones auxiliares para simplificar el desarrollo del programa:
student_t* parse_records(char* records[], int* nr_records);
Esta función acepta como parámetro el listado de registros en formato ASCII pasados como argumento al programa en la línea de comando (records), así como el número de registros (nr_records), y devuelve la representación binaria en memoria de los mismos. Esta representación será un array de estructuras cuya memoria ha de reservarse con malloc() dentro de la propia función.
records
nr_records
malloc()
int dump_entries(student_t* entries, int nr_entries, FILE* students)
La función vuelca al fichero binario ya abierto (students) los registros de estudiantes pasados como parámetro (entries). Para maximizar la reutilización de código, esta función NO escribirá en el fichero la cabecera numérica que indica el número el número de registros.
students
entries
student_t* read_student_file(FILE* students, int* nr_entries)
Esta función lee toda la información de un fichero binario de registros de estudiantes ya abierto, y devuelve tanto la información de la cabecera (parámetro de retorno nr_entries), como el array de registros de estudiantes (valor de retorno de la función). La memoria del array que se retorna debe reservarse con malloc() dentro de la propia función.
nr_entries
char* loadstr(FILE* students)
La función lee una cadena de caracteres terminada en '\0' del fichero cuyo descriptor se pasa como parámetro, reservando la cantidad de memoria adecuada para la cadena leída.
'\0'
## List project's files and compile program usuarioso@debian:~/student-records$ ls defs.h Makefile student-records.c usuarioso@debian:~/student-records$ make gcc -c -Wall -g student-records.c -o student-records.o gcc -g -o student-records student-records.o ## Create a new 2-record file and dump contents of the associated binary file usuarioso@debian:~/student-records$ ./student-records -f database -c \ > 27:67659034X:Chris:Rock 34:78675903J:Antonio:Banderas 2 records written succesfully usuarioso@debian:~/student-records$ xxd database 00000000: 0200 0000 1b00 0000 3637 3635 3930 3334 ........67659034 00000010: 5800 4368 7269 7300 526f 636b 0022 0000 X.Chris.Rock.".. 00000020: 0037 3836 3735 3930 334a 0041 6e74 6f6e .78675903J.Anton 00000030: 696f 0042 616e 6465 7261 7300 io.Banderas. ## Add 2 new registers at the end usuarioso@debian:~/student-records$ ./student-records -f database -a \ > 3:58943056J:Santiago:Segura 4:6345239G:Penelope:Cruz 2 extra records written succesfully usuarioso@debian:~/student-records$ xxd database 00000000: 0400 0000 1b00 0000 3637 3635 3930 3334 ........67659034 00000010: 5800 4368 7269 7300 526f 636b 0022 0000 X.Chris.Rock.".. 00000020: 0037 3836 3735 3930 334a 0041 6e74 6f6e .78675903J.Anton 00000030: 696f 0042 616e 6465 7261 7300 0300 0000 io.Banderas..... 00000040: 3538 3934 3330 3536 4a00 5361 6e74 6961 58943056J.Santia 00000050: 676f 0053 6567 7572 6100 0400 0000 3633 go.Segura.....63 00000060: 3435 3233 3947 0050 656e 656c 6f70 6500 45239G.Penelope. 00000070: 4372 757a 00 Cruz. ## Try to add an entry that matches an existing student ID $ ./student-records -f database -a 3:58943056J:Antonio:Segura Found duplicate student_id 3 ## List all the entries in the file usuarioso@debian:~/student-records$ ./student-records -f database -l [Entry #0] student_id=27 NIF=67659034X first_name=Chris last_name=Rock [Entry #1] student_id=34 NIF=78675903J first_name=Antonio last_name=Banderas [Entry #2] student_id=3 NIF=58943056J first_name=Santiago last_name=Segura [Entry #3] student_id=4 NIF=6345239G first_name=Penelope last_name=Cruz ## Search for specific entries usuarioso@debian:~/student-records$ ./student-records -f database -q -i 7 No entry was found usuarioso@debian:~/student-records$ ./student-records -f database -q -i 34 [Entry #1] student_id=34 NIF=78675903J first_name=Antonio last_name=Banderas usuarioso@debian:~/student-records$ ./student-records -f database -q -n 6345239G [Entry #3] student_id=4 NIF=6345239G first_name=Penelope last_name=Cruz
Con el fin de practicar el uso del shell, se pide al alumno que desarrolle su propio script bash para la comprobación de la funcionalidad del programa desarrollado en el ejercicio anterior.
Antes de elaborar y ejecutar el script se preparará un fichero de texto llamado records.txt en el mismo directorio que el programa, que debe incluir un conjunto de registros de estudiantes en texto plano y separados por un salto de línea, como el el siguiente ejemplo:
27:67659034X:Chris:Rock 34:78675903J:Antonio:Banderas 3:58943056J:Santiago:Segura 4:6345239G:Penelope:Cruz
El script deberá seguir el siguiente esquema:
student-records
cat
for
xxd
-q -n
cut
En los distintos puntos del script se ha de comprobar que cada invocación del programa student-records devuelve 0. En caso de que se produzca un error, la ejecución del script debe abortarse en ese momento y devolver 1.